/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package Linker;

import Common.Dump;
import Common.ObjectFileRecord;
import Common.Utilities;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 *
 * @author Kyle
 */
public class Linker
{
    List<ObjectFileRecord> Records;
    LoaderFile Output;
    Map<String, LinkerSymbol> Symbols;
    String ProgramName;

    private int LC;

    public Linker()
    {
        Records = new ArrayList<ObjectFileRecord>();
        Symbols = new HashMap<String, LinkerSymbol>();
        
        LC = 0;
    }

    public void AddInput(String file)
    {
        try
        {
            BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(file)));
            
            while (in.ready())
            {
                ParseLine(in.readLine());
            }
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

    private void ParseLine(String line)
    {
        ObjectFileRecord record = new ObjectFileRecord();
        record.Data.addAll(Arrays.asList(line.split(":")));
        record.Type = record.Data.get(0);
        record.ProgramName = record.Data.get(record.Data.size() - 1);

        System.out.println(line);
        if (ValidateRecord(record))
        {
            Records.add(record);

            if (record.Type.equals("H"))
            {
                LinkerSymbol symbol = new LinkerSymbol(record.Data.get(1));
                symbol.Assembler_LC = Integer.valueOf(record.Data.get(2), 16);
                symbol.Length = Integer.valueOf(record.Data.get(3), 16);
                symbol.Usage = "Pgm_name";
                symbol.Linker_LC = LC;
                symbol.ProgramName = record.ProgramName;
                if (!Symbols.containsKey(symbol.Label))
                {
                    Symbols.put(symbol.Label, symbol);
                }
                else
                {
                    System.out.println("Label " + symbol.Label + " already found!");
                }

                LC += symbol.Length;
                
            }
            else if (record.Type.equals("L"))
            {
                LinkerSymbol symbol = new LinkerSymbol(record.Data.get(1));
                symbol.Assembler_LC = Integer.valueOf(record.Data.get(2), 16);
                symbol.Length = 1;
                symbol.Usage = "label";
                symbol.ProgramName = record.ProgramName;

                LinkerSymbol header = Symbols.get(record.ProgramName);
                if (symbol.Assembler_LC < header.Assembler_LC || symbol.Assembler_LC > (header.Assembler_LC + header.Length))
                {
                    System.out.println("Warning: Linking record found outside of bounds of the program.");
                }
                else
                {
                    symbol.Linker_LC = symbol.Assembler_LC + LC - header.Length - header.Assembler_LC;
                    if (!Symbols.containsKey(symbol.Label))
                    {
                        Symbols.put(symbol.Label, symbol);
                    }
                    else
                    {
                        System.out.println("Label " + symbol.Label + " already found!");
                    }

                }

                
            }

        }
    }

    public void ComputeOutput()
    {
        Output = new LoaderFile();
        ProgramName = Records.get(0).ProgramName;
        ObjectFileRecord header = new ObjectFileRecord();
        header.Type = "H";
        header.Data.add(ProgramName);
        header.Data.add(Utilities.IntTo4Hex(0));
        header.Data.add(Utilities.IntTo4Hex(0));
        header.Data.add(Utilities.IntTo4Hex(LC));
        header.Data.add(new SimpleDateFormat("yyyyDD,HH,mm,ss").format(new Date()));
        Output.AddRecord("H", header);

        for (ObjectFileRecord record : Records)
        {
            ComputeRecord(record);
        }
        header.Data.add(Utilities.IntTo4Hex(Output.NumRecords));
        header.Data.add(Utilities.IntTo4Hex(Output.NumTRecords));
        header.Data.add("FFA-LLM");
        header.Data.add(String.format("%04d", LinkerMain.VERSION));
        header.Data.add(ProgramName);

        ObjectFileRecord end = new ObjectFileRecord();
        end.Type = "E";
        end.Data.add(ProgramName);
        Output.AddRecord("E", end);
                
    }

    private void ComputeRecord(ObjectFileRecord record)
    {
        if (record.Type.equals("T"))
        {
            int address = Integer.valueOf(record.Data.get(1), 16);
            int diff = Symbols.get(record.ProgramName).Linker_LC - Symbols.get(record.ProgramName).Assembler_LC;
            address += diff;

            int data = Integer.valueOf(record.Data.get(2), 16);

            if (record.Data.get(3).equals("R"))
            {
                data += diff;
            }
            else if (record.Data.get(3).equals("M"))
            {
                List<ObjectFileRecord> MRecords = GetMRecords(record.Data.get(1), record.ProgramName);
                for (ObjectFileRecord mRecord : MRecords)
                {
                    data += EvaluateModifyRecord(mRecord, diff);
                }

            }
            ObjectFileRecord newRecord = new ObjectFileRecord();
            newRecord.Type = "T";
            newRecord.Data.add(Utilities.IntTo4Hex(address));
            newRecord.Data.add(Utilities.IntTo4Hex(data));
            newRecord.Data.add(ProgramName);

            Output.AddRecord("T", newRecord);

        }
    }

    private List<ObjectFileRecord> GetMRecords(String hex, String module)
    {
        List<ObjectFileRecord> result = new ArrayList<ObjectFileRecord>();
        for (ObjectFileRecord record : Records)
        {
            if (record.Type.equals("M") && record.Data.get(1).equals(hex) && record.ProgramName.equals(module))
            {
                result.add(record);
            }
        }

        return result;
    }

    private int EvaluateModifyRecord(ObjectFileRecord mrecord, int diff)
    {
        int result = 0;
        int num = Integer.valueOf(mrecord.Data.get(3));
        for (int i=0; i < num; i++)
        {
            if (mrecord.Data.get(i*2 + 4).equals("+"))
            {
                if (Symbols.containsKey(mrecord.Data.get(i*2 + 5)))
                {
                    if (Symbols.get(mrecord.Data.get(i*2 + 5)).ProgramName.equals(mrecord.ProgramName))
                    {
                        result += diff;
                    }
                    else
                    {
                        result += Symbols.get(mrecord.Data.get(i*2 + 5)).Linker_LC;
                    }
                }
                else
                {
                    System.out.println("Unable to find symbol: " + mrecord.Data.get(i*2 + 5) + ". Assuming local variable.");
                    result += diff;
                }

            }
            else if (mrecord.Data.get(i*2 + 4).equals("-"))
            {
                result -= Symbols.get(mrecord.Data.get(i*2 + 5)).Linker_LC;
            }
            else
            {
                System.out.println("Unknown operator found: " + mrecord.Data.get(i*2 + 4));
            }
        }

        return result;
    }

    public void PrintRecords()
    {
        for (ObjectFileRecord record : Records)
        {
            record.PrintRecordNoType(System.out);
        }
    }

    public void PrintDump()
    {
        String mem[] = new String[1024];

        for (ObjectFileRecord record : Output.Records)
        {
            mem[Integer.valueOf(record.Data.get(0), 16)] = record.Data.get(1);
        }
        Dump dump = new Dump(mem);
        dump.PrintMemoryDump();
    }

    public void PrintSymbols()
    {
        System.out.printf("%-32s  Assembler_LC  Usage     Linker_LC  Length\n", "Label");
        for (LinkerSymbol symbol : Symbols.values())
        {
            System.out.printf("%-32s  ", symbol.Label);
            System.out.printf("%04X          ", symbol.Assembler_LC);
            System.out.printf("%-8s  ", symbol.Usage);
            System.out.printf("%04X       ", symbol.Linker_LC);
            System.out.printf("%04X", symbol.Length);
            System.out.println();
        }
    }

    private boolean ValidateRecord(ObjectFileRecord record)
    {
        boolean result = true;
        if(record.Type.equals("H"))
        {
            String name = record.Data.get(1);
            if(!Utilities.ValidateLabel(name))
            {
                result = false;
                System.out.printf("Invalid Program Name. Program name must be a-z, A-Z, and 0-9 after the first character. 32 character max\n"); 
            }
            String address = record.Data.get(2);
            if(!Utilities.ValidateHexRange(address,0,0xFFFF))
            {
                result = false;
                System.out.printf("Invalid address. Must be in the range 0 - 65535\n");
            }
            String length = record.Data.get(3);
            if(!Utilities.ValidateHexRange(length,0,1023))
            {
                result = false;
                System.out.printf("Invalid length of program. Must be in the range 0 -1023\n");
            }
            String startaddress = record.Data.get(4);
            if(!Utilities.ValidateHexRange(startaddress,0,0xFFFF))
            {
                result = false;
                System.out.printf("Invalid start address. Must be in the range 0 - 65535\n");
            }
            String version = record.Data.get(6);
            if(!Utilities.ValidateIntRange(version,0,9999))
            {
                result = false;
                System.out.printf("Invalid number of versions. Must be in the range 0 - 65535\n");
            }
            String records = record.Data.get(7);
            if(!Utilities.ValidateHexRange(records,0,0xFFFF))
            {
                result = false;
                System.out.printf("Invalid number of records. Must be in the range 0 - 65535\n");
            }
            String linkingrecords = record.Data.get(8);
            if(!Utilities.ValidateHexRange(linkingrecords,0,0xFFFF))
            {
                result = false;
                System.out.printf("Invalid number of linking records. Must be in the range 0 - 65535\n");
            }
            String textrecords = record.Data.get(9);
            if(!Utilities.ValidateHexRange(textrecords,0,0xFFFF))
            {
                result = false;
                System.out.printf("Invalid number of text records. Must be in the range 0 - 65535\n");
            }
            String modificationrecs = record.Data.get(10);
            if(!Utilities.ValidateHexRange(modificationrecs,0,0xFFFF))
            {
                result = false;
                System.out.printf("Invalid number of modification records. Must be in the range 0 - 65535\n");
            }
            String name2 = record.Data.get(12);
            if(!name2.equals(name))
            {
                result = false;
                System.out.printf("Program names provided in the start and the end of the H record are not the same\n");
            }
            if(record.Data.size() != 13)
            System.out.printf("Invalid format for H record. Check format\n");
        }
        else if(record.Type.equals("L"))
        {
           String entry = record.Data.get(1);
           if(!Utilities.ValidateLabel(entry))
           {
                result = false;
                System.out.printf("Entry name is invalid. Must follow conventions for label - must be a-z, A-Z, and 0-9 after the first character. 32 character max\n");
           } 
           String location = record.Data.get(2);
           if(!Utilities.ValidateHexRange(location, 0, 0xFFFF))
           {
                result = false;
                System.out.printf("Location of linking record within the program is invalid. Must be in the range 0 - 65535\n");
           } 
           String name = record.Data.get(3);
           if(!Utilities.ValidateLabel(name))
           {
                result = false;
                System.out.printf("Program name provided in the linking record is invalid. must be a-z, A-Z, and 0-9 after the first character. 32 character max\n");
           }
           if(record.Data.size() != 4)
           System.out.printf("Invalid format for L record. Check format\n");
        }
        else if(record.Type.equals("T"))
        {
           String location = record.Data.get(1);
           if(!Utilities.ValidateHexRange(location, 0, 0xFFFF))
           {
                result = false;
                System.out.printf("Location of linking record within the program is invalid. Must be in the range 0 - 65535\n");
           } 
           String instruction = record.Data.get(2);
           if(!Utilities.ValidateHexRange(instruction,0, 0xFFFF))
           {
                result = false;
                System.out.printf("Invalid instruction. Must be in the range 0 - 65535");
           }
           String StatusFlag = record.Data.get(3);
           if(!(StatusFlag.equals("A") || StatusFlag.equals("R") || StatusFlag.equals("M")))
           {
                result = false;
                System.out.printf("Invalid address status flag. Must be A, R or M\n");
           }
           String adjustments = record.Data.get(4);
           if(!Utilities.ValidateIntRange(adjustments, 0, 9))
           {
               result = false;
               System.out.printf("Invalid number of M adjustments provided. Must be in the range 0 - 9\n");
           }
           String name = record.Data.get(5);
           if(!Utilities.ValidateLabel(name))
           {
                result = false;
                System.out.printf("Program name provided in the record is invalid. must be a-z, A-Z, and 0-9 after the first character. 32 character max\n");
           }
           if(record.Data.size() != 6)
           System.out.printf("Invalid format for T record. Check format\n");
        }
        else if(record.Type.equals("M"))
        {
           String location = record.Data.get(1);
           if(!Utilities.ValidateHexRange(location, 0, 0xFFFF))
           {
                result = false;
                System.out.printf("Location of modification record within the program is invalid. Must be in the range 0 - 65535\n");
           }
           String instruction = record.Data.get(2);
           if(!Utilities.ValidateHexRange(instruction,0, 0xFFFF))
           {
                result = false;
                System.out.printf("Invalid instruction/ data word. Must be in the range 0 - 65535\n");
           }
           String adjustments = record.Data.get(3);
           if(!Utilities.ValidateIntRange(adjustments, 0, 9))
           {
               result = false;
               System.out.printf("Invalid number of adjustments provided. Must be in the range 1 - 15\n");
           }
           int adjust = Integer.valueOf(adjustments);
           int i = 0;
           while (i < adjust)
           {
               String sign = record.Data.get(4+i*2);
               if(!(sign.equals("+") || sign.equals("-")))
               {
                   result = false;
                   System.out.printf("Invalid sign. Must be '+' or '-'\n");
               }

               String label = record.Data.get(5+i*2);
               if(!Utilities.ValidateLabel(label))
               {
                   result = false;
                   System.out.printf("label name to be used as index to linker symbol table is invalid. must be a-z, A-Z, and 0-9 after the first character. 32 character max\n");
               }
               i+=1;
           }
           String name = record.Data.get(4 + (2*adjust));
           if(!Utilities.ValidateLabel(name))
           {
                result = false;
                System.out.printf("Program name provided in the record is invalid. must be a-z, A-Z, and 0-9 after the first character. 32 character max\n");
           }
           if(record.Data.size() != (5 + (2*adjust)))
           System.out.printf("Invalid format for M record. Check format\n");
        }
        else if(record.Type.equals("E"))
        {
           String name = record.Data.get(1);
           if(!Utilities.ValidateLabel(name))
           {
                result = false;
                System.out.printf("Program name provided in the record is invalid. must be a-z, A-Z, and 0-9 after the first character. 32 character max\n");
           }
           if(record.Data.size() != 2)
           System.out.printf("Invalid format for E record. Check format\n");
        }
        else
        {
            result = false;
            System.out.printf("Invalid record type. Record type must be 'H', L', 'T', 'M' or 'E'\n");
        }
        return result;
    }
}
